/*******************************************************************************
 * Copyright (c) 2009-2011, 2013 Luaj.org. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/
package org.luaj.vm2;

import org.luaj.vm2.LuaTable.Slot;
import org.luaj.vm2.LuaTable.StrongSlot;

import java.lang.ref.WeakReference;

/**
 * Subclass of {@link LuaTable} that provides weak key and weak value semantics.
 * <p>
 * Normally these are not created directly, but indirectly when changing the mode
 * of a {@link LuaTable} as lua script executes.
 * <p>
 * However, calling the constructors directly when weak tables are required from
 * Java will reduce overhead.
 */
class WeakTable implements Metatable {

    boolean weakkeys, weakvalues;
    LuaValue backing;

//	public static LuaTable make(boolean weakkeys, boolean weakvalues) {
//		LuaString mode;
//		if ( weakkeys && weakvalues ) {
//			mode = LuaString.valueOf("kv");
//		} else if ( weakkeys ) {
//			mode = LuaString.valueOf("k");
//		} else if ( weakvalues ) {
//			mode = LuaString.valueOf("v");
//		} else {
//			return LuaTable.tableOf();
//		}
//		LuaTable table = LuaTable.tableOf();
//		LuaTable mt = LuaTable.tableOf(new LuaValue[] { LuaValue.MODE, mode });
//		table.setmetatable(mt);
//		return table;
//	}

    /**
     * Construct a table with weak keys, weak values, or both
     *
     * @param weakkeys   true to let the table have weak keys
     * @param weakvalues true to let the table have weak values
     */
    WeakTable(boolean weakkeys, boolean weakvalues, LuaValue backing) {
        this.weakkeys = weakkeys;
        this.weakvalues = weakvalues;
        this.backing = backing;
    }

    public boolean useWeakKeys() {
        return weakkeys;
    }

    public boolean useWeakValues() {
        return weakvalues;
    }

    public LuaValue toLuaValue() {
        return backing;
    }

    public Slot entry(LuaValue key, LuaValue value) {
        value = value.strongvalue();
        if (value == null)
            return null;
        if (weakkeys && !(key.isnumber() || key.isstring() || key.isboolean())) {
            if (weakvalues && !(value.isnumber() || value.isstring() || value.isboolean())) {
                return new WeakKeyAndValueSlot(key, value, null);
            } else {
                return new WeakKeySlot(key, value, null);
            }
        }
        if (weakvalues && !(value.isnumber() || value.isstring() || value.isboolean())) {
            return new WeakValueSlot(key, value, null);
        }
        return LuaTable.defaultEntry(key, value);
    }

    static abstract class WeakSlot implements Slot {

        protected Object key;
        protected Object value;
        protected Slot next;

        WeakSlot(Object key, Object value, Slot next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public abstract int keyindex(int hashMask);

        public abstract Slot set(LuaValue value);

        public StrongSlot first() {
            LuaValue key = strongkey();
            LuaValue value = strongvalue();
            if (key != null && value != null) {
                return new LuaTable.NormalEntry(key, value);
            } else {
                this.key = null;
                this.value = null;
                return null;
            }
        }

        public StrongSlot find(LuaValue key) {
            StrongSlot first = first();
            return (first != null) ? first.find(key) : null;
        }

        public boolean keyeq(LuaValue key) {
            StrongSlot first = first();
            return (first != null) && first.keyeq(key);
        }

        public Slot rest() {
            return next;
        }

        public int arraykey(int max) {
            // Integer keys can never be weak.
            return 0;
        }

        public Slot set(StrongSlot target, LuaValue value) {
            LuaValue key = strongkey();
            if (key != null && target.find(key) != null) {
                return set(value);
            } else if (key != null) {
                // Our key is still good.
                next = next.set(target, value);
                return this;
            } else {
                // our key was dropped, remove ourselves from the chain.
                return next.set(target, value);
            }
        }

        public Slot add(Slot entry) {
            next = (next != null) ? next.add(entry) : entry;
            if (strongkey() != null && strongvalue() != null) {
                return this;
            } else {
                return next;
            }
        }

        public Slot remove(StrongSlot target) {
            LuaValue key = strongkey();
            if (key == null) {
                return next.remove(target);
            } else if (target.keyeq(key)) {
                this.value = null;
                return this;
            } else {
                next = next.remove(target);
                return this;
            }
        }

        public Slot relink(Slot rest) {
            if (strongkey() != null && strongvalue() != null) {
                if (rest == null && this.next == null) {
                    return this;
                } else {
                    return copy(rest);
                }
            } else {
                return rest;
            }
        }

        public LuaValue strongkey() {
            return (LuaValue) key;
        }

        public LuaValue strongvalue() {
            return (LuaValue) value;
        }

        protected abstract WeakSlot copy(Slot next);
    }

    static class WeakKeySlot extends WeakSlot {

        int keyhash;

        WeakKeySlot(LuaValue key, LuaValue value, Slot next) {
            super(weaken(key), value, next);
            keyhash = key.hashCode();
        }

        WeakKeySlot(WeakKeySlot copyFrom, Slot next) {
            super(copyFrom.key, copyFrom.value, next);
            this.keyhash = copyFrom.keyhash;
        }

        public int keyindex(int mask) {
            return LuaTable.hashmod(keyhash, mask);
        }

        public Slot set(LuaValue value) {
            this.value = value;
            return this;
        }

        public LuaValue strongkey() {
            return strengthen(key);
        }

        protected WeakSlot copy(Slot rest) {
            return new WeakKeySlot(this, rest);
        }
    }

    static class WeakValueSlot extends WeakSlot {

        WeakValueSlot(LuaValue key, LuaValue value, Slot next) {
            super(key, weaken(value), next);
        }

        WeakValueSlot(WeakValueSlot copyFrom, Slot next) {
            super(copyFrom.key, copyFrom.value, next);
        }

        public int keyindex(int mask) {
            return LuaTable.hashSlot(strongkey(), mask);
        }

        public Slot set(LuaValue value) {
            this.value = weaken(value);
            return this;
        }

        public LuaValue strongvalue() {
            return strengthen(value);
        }

        protected WeakSlot copy(Slot next) {
            return new WeakValueSlot(this, next);
        }
    }

    static class WeakKeyAndValueSlot extends WeakSlot {

        int keyhash;

        WeakKeyAndValueSlot(LuaValue key, LuaValue value, Slot next) {
            super(weaken(key), weaken(value), next);
            keyhash = key.hashCode();
        }

        WeakKeyAndValueSlot(WeakKeyAndValueSlot copyFrom, Slot next) {
            super(copyFrom.key, copyFrom.value, next);
            keyhash = copyFrom.keyhash;
        }

        public int keyindex(int hashMask) {
            return LuaTable.hashmod(keyhash, hashMask);
        }

        public Slot set(LuaValue value) {
            this.value = weaken(value);
            return this;
        }

        public LuaValue strongkey() {
            return strengthen(key);
        }

        public LuaValue strongvalue() {
            return strengthen(value);
        }

        protected WeakSlot copy(Slot next) {
            return new WeakKeyAndValueSlot(this, next);
        }
    }

    /**
     * Self-sent message to convert a value to its weak counterpart
     *
     * @param value value to convert
     * @return {@link LuaValue} that is a strong or weak reference, depending on type of {@code value}
     */
    static LuaValue weaken(LuaValue value) {
        switch (value.type()) {
            case LuaValue.TFUNCTION:
            case LuaValue.TTHREAD:
            case LuaValue.TTABLE:
                return new WeakValue(value);
            case LuaValue.TUSERDATA:
                return new WeakUserdata(value);
            default:
                return value;
        }
    }

    /**
     * Unwrap a LuaValue from a WeakReference and/or WeakUserdata.
     *
     * @param ref reference to convert
     * @return LuaValue or null
     * @see #weaken(LuaValue)
     */
    static LuaValue strengthen(Object ref) {
        if (ref instanceof WeakReference) {
            ref = ((WeakReference) ref).get();
        }
        if (ref instanceof WeakValue) {
            return ((WeakValue) ref).strongvalue();
        }
        return (LuaValue) ref;
    }

    /**
     * Internal class to implement weak values.
     *
     * @see WeakTable
     */
    static class WeakValue extends LuaValue {
        WeakReference ref;

        WeakValue(LuaValue value) {
            ref = new WeakReference(value);
        }

        public int type() {
            illegal("type", "weak value");
            return 0;
        }

        public String typename() {
            illegal("typename", "weak value");
            return null;
        }

        public String toString() {
            return "weak<" + ref.get() + ">";
        }

        public LuaValue strongvalue() {
            Object o = ref.get();
            return (LuaValue) o;
        }

        public boolean raweq(LuaValue rhs) {
            Object o = ref.get();
            return o != null && rhs.raweq((LuaValue) o);
        }
    }

    /**
     * Internal class to implement weak userdata values.
     *
     * @see WeakTable
     */
    static class WeakUserdata extends WeakValue {
        WeakReference ob;
        LuaValue mt;

        WeakUserdata(LuaValue value) {
            super(value);
            ob = new WeakReference(value.touserdata());
            mt = value.getmetatable();
        }

        public LuaValue strongvalue() {
            Object u = ref.get();
            if (u != null)
                return (LuaValue) u;
            Object o = ob.get();
            if (o != null) {
                LuaValue ud = LuaValue.userdataOf(o, mt);
                ref = new WeakReference(ud);
                return ud;
            } else {
                return null;
            }
        }
    }

    public LuaValue wrap(LuaValue value) {
        return weakvalues ? weaken(value) : value;
    }

    public LuaValue arrayget(LuaValue[] array, int index) {
        LuaValue value = array[index];
        if (value != null) {
            value = strengthen(value);
            if (value == null) {
                array[index] = null;
            }
        }
        return value;
    }
}
